﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Channels;
using System.IO;

namespace ChessOnline
{
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.Single)]
    class ChessOnlineService : IChessOnlineService, IPolicyRetriever
    {
        // Send policy files
        Message IPolicyRetriever.GetClientAccessPolicy()
        {
            FileStream filestream = File.OpenRead("ClientAccessPolicy.xml");
            // Either specify ClientAccessPolicy.xml file path properly
            // or put that in \Bin folder of the console application
            XmlReader reader = XmlReader.Create(filestream);            
            Message result = Message.CreateMessage(MessageVersion.None, "", reader);                       
            return result;
        }

        Message IPolicyRetriever.GetCrossDomain()
        {
            FileStream filestream = File.OpenRead("CrossDomain.xml");
            // Either specify ClientAccessPolicy.xml file path properly
            // or put that in \Bin folder of the console application
            XmlReader reader = XmlReader.Create(filestream);
            Message result = Message.CreateMessage(MessageVersion.None, "", reader);
            filestream.Close();
            reader.Close();
            return result;
        }

        /// <summary>
        /// Database of users in system. Used by authorization and registration processes.
        /// </summary>
        private const string usersDatabase = "..\\..\\Users.xml";
        private static ServerLogger logger = new ServerLogger();
        private static Dictionary<string, Game> dGames = new Dictionary<string, Game>();
        private static Dictionary<string, User> dUsers = new Dictionary<string, User>();

        /// <summary>
        /// Find user in users database and create an instance of connection with him.
        /// </summary>
        /// <param name="user_name">User name.</param>
        /// <param name="password">User password.</param>
        /// <returns>User session hash which is needed for all client requests. 
        /// Return "-1" if there is no user with given credentials.</returns>
        public string LogIn(string user_name, string user_password)
        {
            logger.Print("LogIn() called: " + user_name);
            string result = "UserNotFound";

            foreach (var kv in dUsers)
            {
                if (kv.Value.Name == user_name && kv.Value.Password == user_password)
                {// found user! Bring him online!
                    Random rnd = new Random((int)DateTime.Now.Ticks);
                    kv.Value.UserHash = rnd.Next().ToString() + "?" + user_name;
                    kv.Value.IsAuthorized = "true";
                    result = kv.Value.UserHash;
                }
            }

            logger.Print("LogIn() responsed: " + result);
            return result;
        }

        public string LogOut(string user_hash)
        {
            throw new NotImplementedException();
        }

        public string IsAuthorized(string user_hash)
        {
            logger.Print("IsAuthorized() called: " + user_hash.ToString());
            string result = "false";

            foreach (var kv in dUsers)
            {
                if (kv.Value.UserHash == user_hash && kv.Value.IsAuthorized == "true")
                {
                    result = "true";
                }
            }

            logger.Print("IsAuthorized() responded: " + result);
            return result;
        }

        public string Register(string user_name, string user_password)
        {
            logger.Print("Register() called: " + user_name);
            string result = "";

            // Найти есть ли пользователь с заданным именем
            if (dUsers.ContainsKey(user_name))
            {
                result = "UserWithSameNameFound";
            }
            else
            {
                // Создадим нового пользователя
                User usr = new User();
                usr.Name = user_name;
                usr.Password = user_password;
                usr.IsAuthorized = "false";
                usr.UserHash = "";
                dUsers.Add(usr.Name, usr);

                // Теперь залогинимся им
                result = LogIn(user_name, user_password);
            }

            logger.Print("Register() responded: " + result);
            return result;
        }

        public List<GameInfo> GetGamesListAvailable(string user_hash)
        {            
            logger.Print("GetGamesListAvailable() called: " + user_hash);
            List<GameInfo> available_games_list = new List<GameInfo>();

            if (IsAuthorized(user_hash) == "true")
            {
                try
                {
                    foreach (var kv in dGames)
                    {// Завершаем игры с отвалившимся создателем
                        if ((DateTime.Now - kv.Value.CreatorLastAction) >= new TimeSpan(0, 0, 10))
                        {
                            QuitGame(GetUserHash(kv.Value.GameInfo.Creator_name), kv.Value.GameInfo.Game_hash);
                        }
                    }

                    foreach (var kv in dGames)
                    {
                        if (kv.Value.GameInfo.Connected_name == "" && kv.Value.GameInfo.IsStarted == "false")
                        {
                            available_games_list.Add(kv.Value.GameInfo);
                        }
                    }
                }
                catch (Exception exc)
                {
                    logger.Print(exc.Message);
                }
            }

            logger.Print("GetGamesListAvailable() responded: " + available_games_list.Count.ToString());
            return available_games_list;
        }

        /// <summary>
        /// Send id of currently created Game.
        /// </summary>
        /// <param name="user_hash">User hash to verify.</param>
        /// <returns>Id of the game.
        /// "Can't create game." if something went wrong.
        /// "User already created the game."
        /// ""
        /// </returns>
        public string CreateNewGame(string user_hash)
        {
            logger.Print("CreateNewGame() called: " + user_hash);
            string result = "Unknown error.";

            // Verify user
            if (IsAuthorized(user_hash) == "true")
            {
                string user_name = GetUserName(user_hash);
                foreach (KeyValuePair<string, Game> kv in dGames)
                {
                    if (kv.Value.GameInfo.Creator_name == user_name)
                    {
                        result = "User already created the game.";
                    }
                }

                if (result != "User already created the game.")
                {// если игрок не создавал игру
                    try
                    {
                        // добавить игру
                        Game gm = new Game();
                        Random rnd = new Random((int)DateTime.Now.Ticks);
                        gm.GameInfo.Game_hash = rnd.Next().ToString() + "?" + user_name;
                        gm.GameInfo.Creator_name = user_name;
                        gm.GameInfo.Connected_name = "";
                        gm.GameInfo.IsStarted = "false";
                        gm.GameInfo.ActivePlayerHash = user_hash;
                        dGames.Add(gm.GameInfo.Game_hash, gm);

                        result = gm.GameInfo.Game_hash;
                    }
                    catch (Exception exc)
                    {
                        logger.Print(exc.Message);
                    }
                }
            }
            else
            {
                result = "User is not valid.";
            }

            logger.Print("CreateNewGame() responded: " + result);
            return result;
        }

        /// <summary>
        /// Присоединиться к свободной игре.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии пользователя.</param>
        /// <param name="game_info">Информация о выбранной игре.</param>
        /// <returns>"User is not valid." - авторизация не пройдена.
        /// "Game has not been found." - игра с данной информацией не найдена.
        /// "Player joined the game." - пользватель успешно присоединился к выбранной игре.</returns>
        public string JoinGame(string user_hash, GameInfo game_info)
        {
            logger.Print("JoinGame() called: " + user_hash + " " + game_info.Creator_name);
            string result = "User is not valid.";

            if (IsAuthorized(user_hash) == "true")
            {
                result = "Game has not been found.";
                // find game with given information
                if (dGames.ContainsKey(game_info.Game_hash))
                {
                    dGames[game_info.Game_hash].GameInfo.Connected_name = GetUserName(user_hash);
                    dGames[game_info.Game_hash].GameInfo.IsStarted = "true";
                    result = "Player joined the game.";
                }
            }

            logger.Print("JoinGame() responded: " + result);
            return result;
        }

        /// <summary>
        /// Попытка пользователем выделить шахмату на игровом поле.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии пользователя.</param>
        /// <param name="game_hash">Уникальный номер игры.</param>
        /// <returns>Если выводимый лист пуст - то возникла какая-то ошибка.
        /// Если выводимый лист полон - то он содержит адреса клеток, куда может ходить фигура.</returns>
        public List<Coordinate> PickChess(string user_hash, string game_hash, Coordinate coord)
        {
            logger.Print("PickChess() called: " + user_hash + " " + game_hash + " "
                + coord.Literal.ToString() + coord.Number.ToString());
            List<Coordinate> list = new List<Coordinate>();
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    if (dGames[game_hash].GameInfo.ActivePlayerHash == user_hash &&
                        dGames[game_hash].GameInfo.WinnerName == "" && dGames[game_hash].GameInfo.IsStarted == "true")
                    {// Если сейчас ход игрока, запросившего выделение шахматы и ее переходов, а также игра начата, но не закончена
                        if (dGames[game_hash].GameInfo.Creator_name == GetUserName(user_hash))
                        {
                            list = dGames[game_hash].GameLogic.TryPickChess(GetUserName(user_hash), coord, ChessColor.White);
                        }
                        else if (dGames[game_hash].GameInfo.Connected_name == GetUserName(user_hash))
                        {
                            list = dGames[game_hash].GameLogic.TryPickChess(GetUserName(user_hash), coord, ChessColor.Black);
                        }
                    }
                }
            }
            logger.Print("PickChess() responded: " + list.Count.ToString());
            return list;
        }
        
        /// <summary>
        /// Сделать ход.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии пользователя.</param>
        /// <param name="game_hash">Уникальный номер игры.</param>
        /// <param name="old_coord">Старая координата шахматы.</param>
        /// <param name="new_coord">Новая координата шахматы.</param>
        /// <returns>"true" - можно передвинуть шахмату из старой позиции в новую.
        /// "false" - нельзя передвинуть шахмату из старой позиции в новую.</returns>
        public string MoveChess(string user_hash, string game_hash, Coordinate old_coord, Coordinate new_coord)
        {
            logger.Print("MoveChess() called: " + user_hash + " " + game_hash + " "
                + old_coord.Literal.ToString() + new_coord.Number.ToString());
            string result = "User not found.";
            if (IsAuthorized(user_hash) == "true")
            {
                result = "Game not found.";
                if (dGames.ContainsKey(game_hash))
                {
                    result = "Not your turn.";
                    if (dGames[game_hash].GameInfo.ActivePlayerHash == user_hash &&
                        dGames[game_hash].GameInfo.WinnerName == "" && dGames[game_hash].GameInfo.IsStarted == "true")
                    {// Если сейчас ход игрока, запросившего передвижение шахматы, а также игра начата, но не закончена
                        if (dGames[game_hash].GameInfo.Creator_name == GetUserName(user_hash) && old_coord != new_coord)
                        {// Если игрок - создатель
                            result = dGames[game_hash].GameLogic.MoveChess(GetUserName(user_hash), old_coord, new_coord, ChessColor.White);
                            if (result == "true")
                            {// Ход сделан
                                dGames[game_hash].GameInfo.ActivePlayerHash = GetUserHash(dGames[game_hash].GameInfo.Connected_name);
                            }
                            else if (result == "victory")
                            {
                                dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Creator_name;
                            }

                        }
                        else if (dGames[game_hash].GameInfo.Connected_name == GetUserName(user_hash) && old_coord != new_coord)
                        {// Если игрок - подключившийся
                            result = dGames[game_hash].GameLogic.MoveChess(GetUserName(user_hash), old_coord, new_coord, ChessColor.Black);
                            if (result == "true")
                            {// Ход сделан
                                dGames[game_hash].GameInfo.ActivePlayerHash = GetUserHash(dGames[game_hash].GameInfo.Creator_name);
                            }
                            else if (result == "victory")
                            {
                                dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Connected_name;
                            }
                        }
                    }
                }
            }
            logger.Print("MoveChess() responded: " + result);
            return result;
        }

        /// <summary>
        /// Сообщить об уходе игрока из игры.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии игрока.</param>
        /// <param name="game_hash">Уникальный номер игры.</param>
        public void QuitGame(string user_hash, string game_hash)
        {
            logger.Print("QuitGame() called: " + user_hash + " " + game_hash);
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    if (dGames[game_hash].GameInfo.Connected_name == GetUserName(user_hash))
                    {
                        dGames[game_hash].GameInfo.Connected_name = "";
                        if (dGames[game_hash].GameInfo.WinnerName == "")
                        {
                            dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Creator_name;
                        }
                    }
                    if (dGames[game_hash].GameInfo.Creator_name == GetUserName(user_hash))
                    {
                        dGames[game_hash].GameInfo.Creator_name = "";
                        if (dGames[game_hash].GameInfo.WinnerName == "")
                        {
                            dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Connected_name;
                        }
                    }
                    
                    if (dGames[game_hash].GameInfo.Creator_name == "" && dGames[game_hash].GameInfo.Connected_name == "")
                    {// После удаления игрока, проверим не пустая ли игра
                        CloseGame(user_hash, game_hash);
                    }
                }
            }
            logger.Print("QuitGame() ended.");
        }

        /// <summary>
        /// Закрыть игру.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии игрока.</param>
        /// <param name="game_hash">Уникальный номер игры.</param>
        void CloseGame(string user_hash, string game_hash)
        {
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    dGames.Remove(game_hash);
                }
            }
        }
        
        /// <summary>
        /// Обновить информацию об игре.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии игрока.</param>
        /// <param name="game_hash">Уникальный номер игры.</param>
        /// <returns>Организационная информация об игре.
        /// объект с пустыми полями - если случилась какая-то ошибка.</returns>
        public GameInfo UpdateGameInfo(string user_hash, string game_hash)
        {
            logger.Print("UpdateGameInfo() called: " + user_hash + " " + game_hash);
            GameInfo info = new GameInfo();
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    // Запросивший обновление пользователь активен
                    if (dGames[game_hash].GameInfo.Creator_name == GetUserName(user_hash))
                    {
                        dGames[game_hash].CreatorLastAction = DateTime.Now;
                    }
                    else if (dGames[game_hash].GameInfo.Connected_name == GetUserName(user_hash))
                    {
                        dGames[game_hash].ConnectedLastAction = DateTime.Now;
                    }

                    // Проверяем все ли пользователи запрашивали обновление (оно запрашивается раз в 1 секунду) 
                    // в течение небольшого интервала времени (например 10 секунд)
                    if ((DateTime.Now - dGames[game_hash].CreatorLastAction) >= new TimeSpan(0, 0, 10)
                        && dGames[game_hash].GameInfo.WinnerName == "")
                    {
                        dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Connected_name;
                        QuitGame(GetUserHash(dGames[game_hash].GameInfo.Creator_name), game_hash);
                    }
                    if ((DateTime.Now - dGames[game_hash].ConnectedLastAction) >= new TimeSpan(0, 0, 10)
                        && dGames[game_hash].GameInfo.IsStarted != "false")
                    {
                        dGames[game_hash].GameInfo.WinnerName = dGames[game_hash].GameInfo.Creator_name;
                        QuitGame(GetUserHash(dGames[game_hash].GameInfo.Connected_name), game_hash);
                    }

                    // Отправляем обновленную информацию об игре
                    info = dGames[game_hash].GameInfo;
                }
            }
            logger.Print("UpdateGameInfo() responded: " + info.Game_hash);
            return info;
        }

        /// <summary>
        /// Обновить шахматную доску.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии игрока.</param>
        /// <param name="game_hash">Униклаьный номер игры.</param>
        /// <returns>Лист клеток.
        /// null - если возникла ошибка.</returns>
        public List<GameCell> UpdatePositions(string user_hash, string game_hash)
        {
            logger.Print("UpdatePositions() called: " + user_hash + " " + game_hash);
            List<GameCell> cells = new List<GameCell>();
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    cells = dGames[game_hash].GameLogic.GetCells();
                }
            }
            logger.Print("UpdatePositions() responded: " + cells.Count.ToString());
            return cells;
        }

        /// <summary>
        /// Возвращает имя игрока-противника в заданной партии
        /// </summary>
        /// <param name="user_hash">Уникальный идентификатор сессии игрока.</param>
        /// <param name="game_hash">Уникальный идентификатор игры.</param>
        /// <returns></returns>
        public string GetEnemyName(string user_hash, string game_hash)
        {
            logger.Print("GetEnemyName() called: " + user_hash + " " + game_hash);
            string result = "User is not valid.";

            if (IsAuthorized(user_hash) == "true")
            {
                result = "Not found such a game.";
                if (dGames.ContainsKey(game_hash))
                {
                    string myname = GetUserName(user_hash);
                    if (myname == dGames[game_hash].GameInfo.Creator_name)
                    {
                        result = dGames[game_hash].GameInfo.Connected_name;
                    }
                    if (myname == dGames[game_hash].GameInfo.Connected_name)
                    {
                        result = dGames[game_hash].GameInfo.Creator_name;
                    }
                    if (myname == "")
                    {
                        result = "There is no second player in this game.";
                    }
                }                
            }

            logger.Print("GetEnemyName() responded: " + result);
            return result;
        }

        /// <summary>
        /// Служебная функция. Получить имя пользователя по уникальному номеру его сессии.
        /// </summary>
        /// <param name="user_hash">Уникальный номер сессии пользователя.</param>
        /// <returns>"User not found." - пользователь с таким номером сессии не найден.
        /// user_name - имя пользователя.</returns>
        private string GetUserName(string user_hash)
        {
            string result = "User not found.";
            foreach (var kv in dUsers)
            {
                if (kv.Value.UserHash == user_hash)
                {
                    result = kv.Value.Name;
                }
            }

            return result;
        }

        /// <summary>
        /// Служебная функция. Получить уникальный номер сессии пользователя по его имени.
        /// </summary>
        /// <param name="user_name">Уникальный номер сессии игрока.</param>
        /// <returns></returns>
        private string GetUserHash(string user_name)
        {
            string result = "User not found.";
            foreach (var kv in dUsers)
            {
                if (kv.Value.Name == user_name)
                {
                    result = kv.Value.UserHash;
                }
            }

            return result;
        }

        /// <summary>
        /// Служебная функция. Определяет цвет игрока в игре.
        /// </summary>
        /// <param name="user_hash"></param>
        /// <param name="game_hash"></param>
        /// <returns></returns>
        ChessColor GetUserColor(string user_hash, string game_hash)
        {
            ChessColor result = ChessColor.None;
            if (IsAuthorized(user_hash) == "true")
            {
                if (dGames.ContainsKey(game_hash))
                {
                    if (dGames[game_hash].GameInfo.Creator_name == GetUserName(user_hash))
                    {
                        result = ChessColor.Black;
                    }
                    else if (dGames[game_hash].GameInfo.Connected_name == GetUserName(user_hash))
                    {
                        result = ChessColor.White;
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Close all connections and clear all unneeded data in databases.
        /// </summary>
        public static void CloseAllConnections()
        {
            // Закрыть все соединения
            logger.Print("Closing all connections...");

            try
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(usersDatabase);
                XmlNodeList xnList = doc.SelectNodes("/Database/User"); // select all users in collection
                foreach (XmlNode xNode in xnList)
                {// for each user in users collection
                    if (xNode.Attributes["is_authorized"].Value == "true")
                    {// for all online users
                        xNode.Attributes["user_hash"].Value = "";
                        xNode.Attributes["is_authorized"].Value = "false";
                    }
                }
                doc.Save(usersDatabase);

                // Закрыть все игры
                dGames.Clear();

                logger.Print("Connections closed.");
            }
            catch (Exception exc)
            {
                logger.Print(exc.Message);
            }
            
            // Сохранить лог
            logger.SaveLog();
        }

        /// <summary>
        /// Read from XML Users Database to class dictionary.
        /// </summary>
        public static void LoadUsersDatabase()
        {
            logger.Print("LoadUsersDatabase() called.");
            try
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(usersDatabase);
                XmlNodeList xnList = doc.SelectNodes("/Database/User"); // select all users in collection
                foreach (XmlNode xNode in xnList)
                {// for each user in users collection
                    User usr = new User();
                    usr.Name = xNode.Attributes["name"].Value;
                    usr.Password = xNode.Attributes["password"].Value;
                    usr.IsAuthorized = xNode.Attributes["is_authorized"].Value;
                    usr.UserHash = xNode.Attributes["user_hash"].Value;
                    dUsers.Add(usr.Name, usr);
                }
            }
            catch (Exception exc)
            {
                logger.Print(exc.Message);
            }
            logger.Print("LoadUsersDatabase() ended. There is " + dUsers.Count + " users in database.");
        }

        /// <summary>
        /// Dump Users Database to hard drive as XML file.
        /// </summary>
        public static void SaveUsersDatabase()
        {
            logger.Print("SaveUsersDatabase() called: " + dUsers.Count);
            try
            {
                // Clear file database
                XmlDocument doc = new XmlDocument();
                doc.Load(usersDatabase);
                XmlNode xMainNode = doc.SelectSingleNode("/Database");
                xMainNode.RemoveAll();
                doc.Save(usersDatabase);

                // Восстановим имя базы
                XmlAttribute xDatabaseName = doc.CreateAttribute("name");
                xDatabaseName.InnerText = "Users";
                xMainNode.Attributes.Append(xDatabaseName);

                foreach(KeyValuePair<string, User> kv in dUsers)
                {                    
                    XmlNode xUser = doc.CreateElement("User");
                    XmlAttribute xName = doc.CreateAttribute("name");
                    xName.InnerText = kv.Value.Name;
                    XmlAttribute xPassword = doc.CreateAttribute("password");
                    xPassword.InnerText = kv.Value.Password;
                    XmlAttribute xIsAuthorized = doc.CreateAttribute("is_authorized");
                    xIsAuthorized.InnerText = kv.Value.IsAuthorized;
                    XmlAttribute xUserHash = doc.CreateAttribute("user_hash");
                    xUserHash.InnerText = kv.Value.UserHash;

                    xUser.Attributes.Append(xName);
                    xUser.Attributes.Append(xPassword);
                    xUser.Attributes.Append(xIsAuthorized);
                    xUser.Attributes.Append(xUserHash);
                    xMainNode.AppendChild(xUser);
                }
                
                doc.Save(usersDatabase);
            }
            catch (Exception exc)
            {
                logger.Print(exc.Message);
            }
            logger.Print("SaveUsersDatabase() ended.");
        }
    }
}
